﻿namespace Microsoft.VisualStudio.TestTools.UnitTesting
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.IO;
    using System.Linq;
    using System.Reflection;

    /// <summary>
    /// Builds assemblies used by unit tests based on the attributes that decorate the unit tests.
    /// </summary>
    public static class AssemblyBuilder
    {
        private static IDictionary<Type, AssemblyInformation> types = new Dictionary<Type, AssemblyInformation>();

        #region Public Methods
        public static string GetAssemblyFileNameForTestClass(bool debugMode = true)
        {
            var method = GetCallingMethod();
            var assemblyInformation = GetAssemblyInformationForTestClass(method.DeclaringType);

            return assemblyInformation.GetAssemblyFileNameForMethod(method, debugMode);
        }

        public static string GetAssemblyFileNameForTestMethod(bool debugMode = true)
        {
            var method = GetCallingMethod();
            var assemblyBuilderAttributes = new AssemblyBuilderAttributeCollector(method);
            var assemblyInformation = new AssemblyInformation(method.DeclaringType, assemblyBuilderAttributes);

            return assemblyInformation.GetAssemblyFileNameForMethod(method, debugMode);
        }

        public static Type GetAssemblyTypeForTestClass(string typeName, bool debugMode = true)
        {
            var method = GetCallingMethod();
            var assemblyInformation = GetAssemblyInformationForTestClass(method.DeclaringType);
            var assembly = assemblyInformation.GetAssemblyForMethod(method, debugMode);

            return assembly.GetType(typeName, throwOnError: true);
        }

        public static Type GetAssemblyTypeForTestMethod(string typeName, bool debugMode = true)
        {
            var method = GetCallingMethod();
            var assemblyBuilderAttributes = new AssemblyBuilderAttributeCollector(method);
            var assemblyInformation = new AssemblyInformation(method.DeclaringType, assemblyBuilderAttributes);
            var assembly = assemblyInformation.GetAssemblyForMethod(method, debugMode);

            return assembly.GetType(typeName, throwOnError: true);
        }

        /// <summary>
        /// Gets the output directory that assemblies will be built in.
        /// </summary>
        /// <returns>The output directory that assemblies will be built in.</returns>
        public static DirectoryInfo GetOutputDirectory()
        {
            var method = GetCallingMethod();
            var assemblyInformation = GetAssemblyInformationForTestClass(method.DeclaringType);

            return assemblyInformation.GetOutputDirectory(method);
        }
        #endregion Public Methods

        #region Private Methods
        private static AssemblyInformation GetAssemblyInformationForTestClass(Type type)
        {
            AssemblyInformation information;
            if (!AssemblyBuilder.types.TryGetValue(type, out information))
            {
                var assemblyBuilderAttributes = new AssemblyBuilderAttributeCollector(type);
                information = new AssemblyInformation(type, assemblyBuilderAttributes);
                AssemblyBuilder.types.Add(type, information);
            }

            return information;
        }

        private static MethodBase GetCallingMethod()
        {
            var trace = new StackTrace(2);
            var frame = trace.GetFrame(0);

            return frame.GetMethod();
        }

        private static string GetValueOfAttributeWithName(object[] attributes, string name, string defaultValue = null)
        {
            var matchingAttribute =
                (from attribute in attributes.OfType<TestPropertyAttribute>()
                 where name.Equals(attribute.Name, StringComparison.OrdinalIgnoreCase)
                 select attribute)
                .FirstOrDefault();

            return (matchingAttribute != null) ? matchingAttribute.Value : defaultValue;
        }
        #endregion Private Methods
    }
}